package owg.util.opengl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Queue;

import com.jogamp.opengl.GL3;
import com.jogamp.opengl.GLException;

import owg.util.NamedInputStream;
import owg.util.data.Pair;

public class ShaderLib
{
    public enum ShaderType
    {
	VERTEX(GL3.GL_VERTEX_SHADER),
	FRAGMENT(GL3.GL_FRAGMENT_SHADER),
	GEOMETRY(GL3.GL_GEOMETRY_SHADER);

	public final int glType;
	
	ShaderType(int glType)
	{
	    this.glType = glType;
	}
    }
    /**Pair contains OpenGL ID of the shader and the number of shaders that use it, in that order.*/
    private static HashMap<String, Pair<Integer,Integer>> shaders;
    /**Contains shader programs.*/
    private static HashMap<String, ShaderProgram> programs;

    static
    {
	shaders = new HashMap<String, Pair<Integer,Integer>>();
	programs = new HashMap<String, ShaderProgram>();
    }
    /**Load the program represented by the two shader files, if it does not already exist.*/
    public static synchronized ShaderProgram get(EGL3 egl, NamedInputStream vertexFile, NamedInputStream fragmentFile) throws IOException, GLException
    {
	String vertexName = vertexFile.getName();
	String fragmentName = fragmentFile.getName();
	Pair<Integer,Integer> v = shaders.get(vertexName);
	if(v==null)
	{
	    v = new Pair<Integer,Integer>();
	    v.a = loadShader(egl,vertexFile, vertexName, ShaderType.VERTEX);
	    v.b = 1;
	    shaders.put(vertexName,v);
	}
	else
	    v.b++;
	Pair<Integer,Integer> f = shaders.get(fragmentName);
	if(f==null)
	{
	    f = new Pair<Integer,Integer>();
	    f.a = loadShader(egl,fragmentFile, fragmentName, ShaderType.FRAGMENT);
	    f.b = 1;
	    shaders.put(fragmentName,f);
	}
	else
	    v.b++;
	ShaderProgram result = programs.get(v.a+":"+f.a);
	if(result==null)
	{
	    result = linkShader(egl,vertexName,fragmentName,v.a,f.a);
	    programs.put(v.a+":"+f.a, result);
	}
	return result;
    }
    /**Call to remove a shader from the hashmaps. Called automatically when ShaderProgram object is garbage collected.
     * This will delete unused GL resources, but not delete resources that are still in use by other shaders.*/
    static synchronized void remove(EGL3 egl, ShaderProgram program)
    {
	Pair<Integer,Integer> v = shaders.get(program.vertexName);
	assert v.a == program.vertexShader : "Internal error: Vertex shader: "+program+" does not match: "+v;
	Pair<Integer,Integer> f = shaders.get(program.fragmentName);
	assert f.a == program.fragmentShader : "Internal error: Fragment shader: "+program+" does not match: "+f;
	ShaderProgram result = programs.remove(v.a+":"+f.a);
	assert result == program : "Internal error: Shader program: "+program+" does not match: "+result;
	final Queue<Integer> delShaders = new LinkedList<Integer>();
	final int delProgram = program.program;
	v.b--;
	if(v.b == 0)
	{
	    shaders.remove(program.vertexName);
	    delShaders.offer(v.a);
	}
	f.b--;
	if(f.b == 0)
	{
	    shaders.remove(program.fragmentName);
	    delShaders.offer(f.a);
	}
	egl.invokeLater(new Runnable() 
	{
	    @Override
	    public void run() 
	    {
		Integer del;
		while((del = delShaders.poll()) != null)
		    egl.getGL().glDeleteShader(del);
		egl.getGL().glDeleteProgram(delProgram);
	    }
	});
    }

    private static int loadShader(EGL3 egl, NamedInputStream file, String shaderName, ShaderType type) throws IOException, GLException
    {
	GL3 gl = egl.getGL();
	System.out.println("Loading "+type+" Shader: "+shaderName);
	int shader = gl.glCreateShader(type.glType);
	BufferedReader brv = new BufferedReader(new InputStreamReader(file));
	String src = "";
	String line;
	while ((line=brv.readLine()) != null) 
	{
	    src += line + "\n";
	}
	brv.close();
	gl.glShaderSource(shader, 1, new String[]{src}, new int[]{src.length()},0);
	gl.glCompileShader(shader);
	egl.checkError("Compiling "+type+" Shader: "+shaderName);

	if (!gl.glIsShader(shader))
	    throw new GLException(shader+" is not a shader!");
	
	return shader;
    }

    private static ShaderProgram linkShader(EGL3 egl, String vName, String fName, int v, int f) throws GLException
    {
	GL3 gl = egl.getGL();
	int shaderProgram = gl.glCreateProgram();
	gl.glAttachShader(shaderProgram, v);
	gl.glAttachShader(shaderProgram, f);
	gl.glLinkProgram(shaderProgram);
	gl.glValidateProgram(shaderProgram);
	
	int[] status = new int[1];
	gl.glGetProgramiv(shaderProgram, GL3.GL_LINK_STATUS, status, 0);
	if (status[0] != 1) 
	{
	    StringBuilder sb = new StringBuilder();
	    int[] logSize = new int[1];
	    gl.glGetProgramiv(shaderProgram, GL3.GL_INFO_LOG_LENGTH, logSize, 0);
	    int size = logSize[0];
	    sb.append("Program link error: "+vName+":"+fName+" - "+v+":"+f+" \n");
	    if (size > 0) 
	    {
		ByteBuffer byteBuffer = ByteBuffer.allocate(size);
		IntBuffer none = IntBuffer.allocate(1);
		gl.glGetProgramInfoLog(shaderProgram, size, none, byteBuffer);
		byte[] array = byteBuffer.array();
		for (int i = 0; i < array.length-1; i++)
		{
		    byte b = array[i];
		    sb.append((char) b);
		}
	    } 
	    else 
	    {
		sb.append("Unknown");
	    }
	    throw new GLException(sb.toString());
	}

	if (!gl.glIsProgram(shaderProgram))
	    throw new GLException(shaderProgram+" is not a shader program!");

	return new ShaderProgram(egl, vName,fName,shaderProgram,v,f);
    }
}
